package com.bolinfest.editor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.bolinfest.editor.util.NamespaceContextMap;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gdata.client.GoogleService;
import com.google.gdata.client.Service;
import com.google.gdata.client.docs.DocsService;
import com.google.gdata.data.TextConstruct;
import com.google.gdata.data.docs.DocumentListEntry;
import com.google.gdata.data.media.MediaByteArraySource;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.common.xml.XmlWriter;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.ParamPart;

public class FeedProxy extends HttpServlet {

  private static final long serialVersionUID = 1L;

  private static UserService userService = UserServiceFactory.getUserService();

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    XsrfUtil.checkNotXsrf(req);

    String feedUrl = getFeedUrl(req);
    DocsService client = getDocsService();

    try {
      String feedXml = fetchFeed(client, feedUrl);
      resp.setContentType(feedUrl.contains("alt=json") ? "text/javascript" : "text/xml");
      resp.getWriter().print(feedXml);
    } catch (ServiceException e) {
      writeServiceException(resp, e);
    }
  }

  private String getFeedUrl(HttpServletRequest req) {
    String feedUrl = req.getParameter("feedUrl");
    if (feedUrl == null || (!feedUrl.startsWith("http://docs.google.com") &&
        !feedUrl.startsWith("https://docs.google.com"))) {
      throw new IllegalArgumentException("invalid feed URL: " + feedUrl);
    }
    return feedUrl;
  }

  private void writeServiceException(HttpServletResponse resp, ServiceException e) throws IOException {
    // If a ServiceException is thrown, output the HTTP status code and
    // error description.
    resp.setContentType("text/html");
    resp.getWriter().print("<h3>An error occurred while processing " +
        "your request.</h3>");
    resp.getWriter().print("<p>Error code " +
        e.getHttpErrorCodeOverride() + ": " + e.getCodeName() + "</p>");
    e.printStackTrace();
  }

  // TODO(bolinfest): Put this in a more appropriate location.
  static DocsService getDocsService() {
    // The request should valid user and authsub token because it passed the
    // AuthSubFilter.
    DocsService client = new DocsService("docbookeditor");
    User user = userService.getCurrentUser();
    String sessionToken = TokenStore.getToken(user);
    client.setAuthSubToken(sessionToken);
    return client;
  }

  @Override
  protected void doPut(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String contentType = req.getContentType();
    if (contentType.startsWith("multipart/form-data")) {
      doMultipartPut(req, resp);
    } else {
      doPutInternal(req, resp);
    }
  }

  private void doMultipartPut(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
    XsrfUtil.checkNotXsrf(req);

    String feedUrl = getFeedUrl(req);
    DocsService client = getDocsService();

    DocumentListEntry entry;
    try {
      entry = client.getEntry(new URL(feedUrl),
          DocumentListEntry.class);
      // AppEngine limits us to 1MB anyway.
      MultipartParser parser = new MultipartParser(req, 1024 * 1024 * 1);
      parser.readNextPart(); // atom part
      ParamPart contentPart = (ParamPart)parser.readNextPart();
      System.out.println(contentPart);
      byte[] content = contentPart.getValue();

      entry.setMediaSource(new MediaByteArraySource(content, "text/html"));
      DocumentListEntry updatedEntry = entry.updateMedia(true);

      resp.setContentType("text/xml");
      XmlWriter xmlWriter = new XmlWriter(resp.getWriter());
      updatedEntry.generate(xmlWriter, client.getExtensionProfile());
      xmlWriter.flush();
    } catch (MalformedURLException e) {
      throw new ServletException(e);
    } catch (ServiceException e) {
      writeServiceException(resp, e);
      return;
    }
  }

  /**
   * A PUT can be used to update metadata, as shown in this example:
   * http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#UpdatingMetadata
   */
  private void doPutInternal(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
    XsrfUtil.checkNotXsrf(req);

    // For now, an ordinary PUT is only used to update the title.
    // Figure out what is going on below so that a generic PUT can be made.
    updateTitle(req, resp);

    // Why doesn't the below work? It should be sending the same information as
    // the DocsService.

//    // Create the connection and set the headers.
//    URL gdataUrl = new URL(feedUrl);
//    URLConnection gdataConnection = gdataUrl.openConnection();
//    gdataConnection.setDoOutput(true);
//    gdataConnection.setDoInput(true);
//    gdataConnection.addRequestProperty("GData-Version", "3.0");
//    gdataConnection.addRequestProperty("Content-Type",
//        "application/atom+xml");
//    gdataConnection.addRequestProperty("Authorization",
//        req.getHeader("Authorization"));
//
//    // Write the XML data.
//    OutputStream toGoogle = gdataConnection.getOutputStream();
//    CharStreams.copy(new InputStreamReader(req.getInputStream()),
//        new OutputStreamWriter(toGoogle));
//    toGoogle.close();
//
//    // Read the response from the connection.
//    String responseFromGdata = CharStreams.toString(
//        new InputStreamReader(gdataConnection.getInputStream()));
//    resp.addHeader("Content-Type", "text/xml");
//    resp.getWriter().append(responseFromGdata);
  }

  private void updateTitle(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
    String feedUrl = getFeedUrl(req);

    DocsService client = getDocsService();
    try {
      // First, parse the XML into a DOM.
      String titleText = getTitleText(req.getInputStream());

      // Update the title of the entry through the DocsService.
      DocumentListEntry entry = client.getEntry(new URL(feedUrl),
          DocumentListEntry.class);
      entry.setTitle(TextConstruct.plainText(titleText));
      DocumentListEntry updatedEntry = entry.update();

      // Write the response.
      resp.setContentType("text/xml");
      XmlWriter xmlWriter = new XmlWriter(resp.getWriter());
      updatedEntry.generate(xmlWriter, client.getExtensionProfile());
      xmlWriter.flush();
    } catch (ServiceException e) {
      throw new ServletException(e);
    }
  }

  /**
   * From an Atom &lt;entry>, extracts the content of the &lt;title> element.
   * (It is appalling how disgusting XML parsing code with namespaces is.)
   */
  private static String getTitleText(InputStream xml) {
    // Sample value of xml:
    // <?xml version='1.0' encoding='UTF-8'?>
    // <entry xmlns="http://www.w3.org/2005/Atom"
    //        xmlns:gd="http://schemas.google.com/g/2005">
    //   <category scheme="http://schemas.google.com/g/2005#kind"
    //             term="http://schemas.google.com/docs/2007#document"/>
    //   <title>test-html-doc2</title>
    // </entry>

    DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
    dbFactory.setNamespaceAware(true);
    DocumentBuilder dBuilder;
    try {
      dBuilder = dbFactory.newDocumentBuilder();
      Document doc = dBuilder.parse(xml);

      // Extract the <title> element from the DOM.
      XPathFactory factory = XPathFactory.newInstance();
      XPath xpath = factory.newXPath();
      NamespaceContext nsContext = new NamespaceContextMap(
          "atom", "http://www.w3.org/2005/Atom",
          "gd", "http://schemas.google.com/g/2005");
      xpath.setNamespaceContext(nsContext);
      XPathExpression expr = xpath.compile("//atom:title/text()");
      Object result = expr.evaluate(doc, XPathConstants.NODESET);
      NodeList nodes = (NodeList) result;

      if (nodes.getLength() == 1) {
        return nodes.item(0).getNodeValue();
      } else {
        return null;
      }
    } catch (ParserConfigurationException e) {
      throw new RuntimeException(e);
    } catch (SAXException e) {
      throw new RuntimeException(e);
    } catch (IOException e) {
      throw new RuntimeException(e);
    } catch (XPathExpressionException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Fetches and returns the specified feed from the specified service or null
   * if the passed client or feedUrl parameters are null.
   *
   * @throws ServiceException if the passed client is not set up correctly or
   *                          an error is thrown by the Google Data service in
   *                          response to the fetch attempt
   * @throws IOException      if an I/O error prevents a connection from being
   *                          opened or otherwise causes request transmission
   *                          to fail
   */
  static String fetchFeed(GoogleService client, String feedUrl) throws
      ServiceException, IOException {
    if (client == null || feedUrl == null) {
      return null;
    }

    // Attempt to fetch the feed.
    Service.GDataRequest feedRequest = client.createFeedRequest(
        new URL(feedUrl));
    feedRequest.execute();

    return streamToString(feedRequest.getResponseStream());
  }

  static String streamToString(InputStream stream) throws IOException {
    if (stream == null) {
      return null;
    }

    StringBuilder builder = new StringBuilder();
    String line;

    try {
      BufferedReader reader = new BufferedReader(
          new InputStreamReader(stream, "UTF-8"));

      while ((line = reader.readLine()) != null) {
        builder.append(line).append("\n");
      }
    } finally {
      stream.close();
    }

    return builder.toString();
  }
}
